<!DOCTYPE html>
<head>
    <title>网页版-俄罗斯方块</title>
    <meta charset="UTF-8">
    <style>
        body,html {
            padding: 0;
            margin: 0;
        }
        .containter {
            width: 100%;
            padding: 30px;
            box-sizing: border-box;

            display: flex;
            justify-content: center;
        }

        .left {
            font-size: 20px;
        }
        .left p span {
            display: inline-block;
            padding: 0;
            margin: 0 0 0 10px;
            font-weight: bold;
            font-size: 22px;
        }

        #canvasDOM {
            border: 3px solid #444;
            margin: 0 30px;
        }

        .right #smallCanvasDOM {
            height: 160px !important;
        }
        .right .score {
            margin-top: 30px;
            font-size: 20px;
        }
        .right .score #scoreText{
            color: blueviolet;
            font-weight: bold;
            font-size: 24px;
        }
    </style>
</head>
<body>
    <div class="containter">
        <div class="left">
            <p>开始/暂停：<span>空格键</span></p>
            <p>左/右/下：<span>&#8592; &nbsp; &#8594; &nbsp; &#8595;</span></p>
            <p>切换形状：<span>&#8593;</span></p>            
        </div>
        <canvas id="canvasDOM" width="100" height="100">
            <span>游览器不支持 canvas , 替换成包裹的span标签内容</span>
        </canvas>

        <div class="right">
            <canvas id="smallCanvasDOM">
                <span>游览器不支持 canvas , 替换成包裹的span标签内容</span>
            </canvas>

            <div class="score">分数：<span id="scoreText"></span></div>
        </div>
    </div>
</body>

<script src="./js/index.js"></script>
<script>
    // 10 X 21
    let GameBoxArr  = [
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
        [0,0,0,0,0,0,0,0,0,0],
    ]
    let boxWidth = boxHeight = 40  // 每个格子的大小
    let GameBoxArrRows = GameBoxArr.length || 0,  GameBoxArrCols = GameBoxArr&&GameBoxArr[0]?.length || 0
    
    const canvas = document.querySelector('#canvasDOM')
    canvas.width = boxWidth * GameBoxArrCols
    canvas.height = boxHeight * GameBoxArrRows
    const ctx = canvas.getContext('2d')  // 这里的ctx指代的就是canvas渲染上下文

    // GameBoxArr,绘制游戏区域格子盘
    function drawBox() {
        GameBoxArr.forEach((rows, y) => {
            for(let x=0; x<rows.length; x++) {
                // console.log('[row, col]', y, x)

                let num = rows[x]
                // （必须先设置画笔，再去画）
                ctx.fillStyle = colorArr[num] || '#999' // num为0时默认颜色为#999，不同数值对应不同的颜色
                ctx.strokeStyle = '#fff'
                ctx.fillRect(x * boxWidth, y * boxHeight, boxWidth, boxHeight)
                ctx.strokeRect(x * boxWidth, y * boxHeight, boxWidth, boxHeight)

                // ctx.fillStyle = '#fff'
                // ctx.textAlign = 'center'
                // ctx.textBaseline = 'middle'
                // ctx.font = '400 16px PingFangSC-Regular'
                // // (填充文字 x-横坐标 y-纵坐标) 【必填】
                // ctx.fillText(rows[x], (x+1) * boxWidth - boxWidth/2, (y+1) * boxHeight - boxHeight/2)
            }
        })
    }
    drawBox()

    // 下一个生成的"块"
    let nextBlock = []
    function drawBoxSmall(type=0) {
        nextBlock = getRandomBlock()

        let rendSmallArr = []
        nextBlock.forEach((rows, row) => {
            rendSmallArr[row] = [...rows]
            if(type==1) {
                for(let x=0; x<rows.length; x++) {
                    rendSmallArr[row][x] = 0
                }
            }
        })
        
        // 下一个方块展示区域的参数   4 X 4
        let bWidth = bHeight = 40  // 每个格子的大小
        let smallRows = rendSmallArr.length || 0,  smallCols = rendSmallArr&&rendSmallArr[0]?.length || 0
        const smallCanvas = document.querySelector('#smallCanvasDOM')
        smallCanvas.width = bWidth * smallCols
        smallCanvas.height = bHeight * smallRows
        const ctx1 = smallCanvas.getContext('2d')
        
        rendSmallArr.forEach((rows, y) => {
            for(let x=0; x<rows.length; x++) {
                let num = rows[x]
                ctx1.fillStyle = colorArr[num] || '#999'
                ctx1.strokeStyle = '#fff'
                ctx1.fillRect(x * bWidth, y * bHeight, bWidth, bHeight)
                ctx1.strokeRect(x * bWidth, y * bHeight, bWidth, bHeight)
            }
        })
    }
    // 渲染一个空的小盘， 生成"第一个"随机的"块"
    drawBoxSmall(1)

    // 根据随机生成的块nextBlock，生成对应的位置坐标数组
    function birthLocationArr(x=3, y=-4) {
        // y = 0
        // 拿到随机块
        let theBlockArr = nextBlock

        let locationArr = []
        theBlockArr.forEach((rows, row) => {
            let strArr = []
            for(let col=0; col<rows.length; col++) {
                if(rows[col]>0) {
                    strArr.push((y+row) + ',' + (x+col) + ',' + rows[col])  // 'row,col,value' (第一个参数是行，第二个参数是列, 第三个参数是值)
                    
                    // 修改游戏盘的值
                    if(((y+row)>=0 && (y+row)<GameBoxArrRows) && ((x+col)>=0 && (x+col)<GameBoxArrCols)) {
                        GameBoxArr[y+row][x+col] = rows[col]
                    }
                }
                else {
                    strArr.push(0)
                }
            }
            locationArr.push([...strArr])
        })
        
        drawBox()
        return [...locationArr]
    }

    let curLocationArr = birthLocationArr(3, 0)
    setTimeout(() => {
        // 1s之后生成下一个随机的"块"
        drawBoxSmall()
    }, 1000)

    let ScoreText = 0
    document.getElementById('scoreText').innerText = ScoreText
    // 消除一行的基数是10分
    let lineScore = 10
    // 消除格子(判断一行是否填满)
    function eliminateGameBox() {
        // 消除行数
        let eliminateCount = 0
        let midleArr = []
        for(let row = GameBoxArrRows-1; row>=0; row--) {
            let rowCount = 0
            for(let col=0; col<GameBoxArrCols; col++) {
                if(GameBoxArr[row][col]>0) {
                    rowCount++
                }
            }

            // 整行格子的数值都大于0， 表示这一行需要消除
            if(rowCount==GameBoxArrCols) {
                eliminateCount++
            }
            else {
                midleArr.unshift([...GameBoxArr[row]])
            }
        }
        let lineArr = []
        for(let i=0;i<GameBoxArrCols; i++) {
            lineArr.push(0)
        }
        for(let i=0; i<eliminateCount;i++) {
            midleArr.unshift([...lineArr])
        }

        GameBoxArr = [...midleArr]

        // 消除行数==> 修改分数
        if(eliminateCount<=2) {
            ScoreText = ScoreText + lineScore*eliminateCount*1
            document.getElementById('scoreText').innerText = ScoreText
        }
        else if(eliminateCount>=3) {
            ScoreText = ScoreText + lineScore*eliminateCount*2
            document.getElementById('scoreText').innerText = ScoreText
        }
    }

    let timer = null
    clearInterval(timer)
    // 游戏开始和暂停
    function startAndStop() {
        // 开始
        if(!timer) {
            timer =setInterval(() => {
                MoveLocationDraw()
            }, 500)
        }
        // 暂停
        else {
            clearInterval(timer)
            timer = null
        }
    }

    // 把位置数组绘制到游戏盘中
    function changeGameBoxDraw(localArr) {
        // console.log('localArr:', localArr)
        localArr.forEach((rows, row) => {
            for(let col=0; col<rows.length; col++) {
                if(rows[col]!==0) {
                    let str = rows[col].split(',')  // 'row,col,value' (第一个参数是行，第二个参数是列, 第三个参数是值)

                    let curRow = Number(str[0])
                    let curCol = Number(str[1])

                    // 修改游戏盘的值
                    if((curRow>=0 && curRow<GameBoxArrRows) && (curCol>=0 && curCol<GameBoxArrCols)) {
                        GameBoxArr[curRow][curCol] = Number(str[2])
                    }
                }
            }
        })

        // 绘制
        drawBox()
    }

    // 方向移动
    function MoveLocationDraw(direction="bottom") {
        // 标志有没有小格子超出游戏区域外， 0默认没有
        let flag = 0

        // 存储当前块数组的当前位置信息， 当向左/右/下移动到游戏区域外时，则还原到这个数组
        let preBlockArr = []
        
        // 块的数组修改 + 把块在游戏盘中前一个位置清除掉
        curLocationArr.forEach((rows, row) => {
            let oldArr = []
            for(let col=0; col<rows.length; col++) {
                oldArr.push(rows[col])
                if(rows[col]!==0) {
                    let str = rows[col].split(',')  // 'row,col,value' (第一个参数是行，第二个参数是列, 第三个参数是值)

                    let preRow = Number(str[0])
                    let preCol = Number(str[1])

                    // 清除当前数组块 在 "游戏盘" 的值
                    if((preRow>=0 && preRow<GameBoxArrRows) && (preCol>=0 && preCol<GameBoxArrCols)) {
                        GameBoxArr[preRow][preCol] = 0
                    }
                    
                    // 左右移动
                    if(direction=="left" || direction=="right") {
                        let pos = (direction=="left") ? preCol-1 : preCol+1
                        rows[col] = preRow + ',' + pos + ',' + str[2]
                        // 这一步操作导致有小格子溢出游戏区域，则标志一下
                        if((direction=="left" && pos<0) || (direction=="right" && pos>=GameBoxArrCols)) {
                            flag = 1
                        }
                    }
                    // 向下移动
                    else if(direction=="bottom") {
                        let pos = preRow+1
                        rows[col] = pos + ',' + preCol + ',' + str[2]
                        if(pos>=GameBoxArrRows) {
                            flag = 2
                        }
                    }
                }
            }
            preBlockArr.push([...oldArr])
        })

        // 检查新位置数组是否存在与游戏盘中的其他盒子重合的小格子， 有则标志一下flag=2, 生成新的位置数组
        curLocationArr.forEach((rows, row) => {
            for(let col=0; col<rows.length; col++) {
                if(rows[col]!==0) {
                    let str = rows[col].split(',')  // 'row,col,value' (第一个参数是行，第二个参数是列, 第三个参数是值)

                    let preRow = Number(str[0])
                    let preCol = Number(str[1])

                    if((preRow>=0 && preRow<GameBoxArrRows) && (preCol>=0 && preCol<GameBoxArrCols)) {

                        // 如果是向下移动，新位置有重合的小格子，则标志为2， 重置为上一个状态且生成新的位置数组
                        if(GameBoxArr[preRow][preCol]>0 && direction=='bottom') {
                            flag = 2
                        }
                        // 如果是左右移动， 新位置有重叠的小格子，则标志为1，重置为上一个状态
                        else if(GameBoxArr[preRow][preCol]>0 && (direction=='left' || direction=='right')) {
                            flag = 1
                        }
                    }
                }
            }
        })

        // 重要步骤：************** 判断下一个位置的状态 *******************
        if(flag) {
            // 如果这一步的操作导致块数组溢出游戏区域外 或者 新位置中小格子与游戏盘中的小格子有重合的， 就把块数组重置为前一个状态
            preBlockArr.forEach((rows, row) => {
                curLocationArr[row] = [...preBlockArr[row]]
            })

            if(flag == 2) {
                // curLocationArr 中的小格子已落定， 此时检查小格子在游戏盘中的数量
                let inBoxCount = 0
                curLocationArr.forEach((rows, row) => {
                    for(let col=0; col<rows.length; col++) {
                        if(rows[col]!==0) {
                            let str = rows[col].split(',')  // 'row,col,value' (第一个参数是行，第二个参数是列, 第三个参数是值)
                            let preRow = Number(str[0])
                            let preCol = Number(str[1])

                            if((preRow>=0 && preRow<GameBoxArrRows) && (preCol>=0 && preCol<GameBoxArrCols)) {
                                inBoxCount++
                            }
                        }
                    }
                })
                
                // 游戏还未结束， 4个小格子都在游戏棋盘中
                if(inBoxCount==4) {
                    // 保留之前的绘制
                    changeGameBoxDraw(curLocationArr)
                    
                    // 函数生成的每一个数组都是存储地址不一样的
                    curLocationArr = birthLocationArr()

                    setTimeout(() => {
                        // 生成下一个方块之后，当前方块已成定局，判断游戏盘可以消除的部分
                        eliminateGameBox()
                        // console.log('GameBoxArr:', GameBoxArr)
                    },)

                    setTimeout(() => {
                        drawBoxSmall()
                    }, 1000)
                }
                else {
                    startAndStop()
                    alert('游戏结束')
                    location.reload()
                }
            }
            return
        }
        
        // 重新绘制： 把新的块数组的信息加入到游戏棋盘中
        changeGameBoxDraw(curLocationArr)
    }

    // 变换形状
    function changeShape() {
        let curRow, curCol, value = 0

        // 1.0 preBlockArr存储当前块数组curLocationArr的当前位置信息， 当变换形状失败时则还原到这个数组
        let preBlockArr = []
        curLocationArr.forEach((rows, row) => {
            preBlockArr.push([...rows])
        })

        // 2.0 存储curLocationArr顺时针旋转90度的数组
        let newQueue = []
        for(let col=0; col<curLocationArr[0].length; col++) {
            let oneQueue = []
            for(let row=0; row<curLocationArr.length; row++) {

                if(curLocationArr[row][col] !== 0) {
                    let str = curLocationArr[row][col].split(',')  // 'row,col,value' (第一个参数是行，第二个参数是列, 第三个参数是值)
                    curRow = Number(str[0]) - row
                    curCol = Number(str[1]) - col  // 位置盘的(0,0) 在游戏盘中的位置(curRow, curCol)
                    value = Number(str[2])
                }

                // unshift从队头插入 
                oneQueue.unshift(curLocationArr[row][col])
            }
            newQueue.push([...oneQueue])
        }
        // 设置旋转90度数组的位置信息
        newQueue.forEach((rows, row) => {
            for(let col=0; col<rows.length; col++) {
                if(rows[col] !== 0) {
                    rows[col] = (curRow+row) + ',' + (curCol+col) + ',' + value
                }
            }
        })

        // 3.0 清空位置变换前的数组curLocationArr在游戏盘中的渲染
        curLocationArr.forEach((rows, row) => {
            for(let col=0; col<rows.length; col++) {
                if(rows[col] !==0 ) {
                    let str = rows[col].split(',')  // 'row,col,value' (第一个参数是行，第二个参数是列, 第三个参数是值)

                    let preRow = Number(str[0])
                    let preCol = Number(str[1])

                    if((preRow>=0 && preRow<GameBoxArrRows) && (preCol>=0 && preCol<GameBoxArrCols)) {
                        GameBoxArr[preRow][preCol] = 0
                    }
                }
            }
        })

        // 4.0 判断变换之后的位置时候合理，或者是否可以变换
        let flagChange = 0
        // 检查新位置数组是否存在与游戏盘中的其他盒子重合的小格子
        newQueue.forEach((rows, row) => {
            for(let col=0; col<rows.length; col++) {
                if(rows[col]!==0) {
                    let str = rows[col].split(',')  // 'row,col,value' (第一个参数是行，第二个参数是列, 第三个参数是值)

                    let preRow = Number(str[0])
                    let preCol = Number(str[1])

                    if((preRow>=0 && preRow<GameBoxArrRows) && (preCol>=0 && preCol<GameBoxArrCols)) {

                        if(GameBoxArr[preRow][preCol]>0) {
                            flagChange = 1
                        }
                    }
                    // 变换后的数组有溢出游戏盘的小格子，则标志一下，还原的变换前的位置
                    else {
                        flagChange = 1
                    }
                }
            }
        })

        // 变换失败
        if(flagChange==1) {
            curLocationArr = [...preBlockArr]
        }
        // 变换后的位置：变换成功
        else {
            curLocationArr = [...newQueue]
            // 重新绘制： 把新的块数组的信息加入到游戏棋盘中
            changeGameBoxDraw(curLocationArr)
        }

        // console.log(curRow, curCol, value)
        // console.log(curLocationArr, newQueue)
    }

    // 添加监听按下键盘事件
    document.addEventListener("keydown", function(event) {
        let key = event.key;
        let keyCode = event.keyCode;
        // 在这里处理按键事件
        // console.log("按下键：" + key + ", keyCode: " + keyCode)

        // 空格键  开始/暂停
        if(keyCode==32) {
            startAndStop()
        }
        // 上
        if(keyCode==38) {
            changeShape()
        }
        // 下
        else if(keyCode==40) { 
            MoveLocationDraw('bottom')
        }
        // 左
        else if(keyCode==37) {
            MoveLocationDraw('left')
        }
        // 右
        else if(keyCode==39) {
            MoveLocationDraw('right')
        }
    })
</script>